本來預計今天要寫 useDeviceOrientation,但發現 useDeviceOrientation 有用到 useSupported,而 useSupported 有用到 useMounted,所以今天先把 useMounted & useSupported 一起看完,明天的話應該會 useDeviceOrientation & useScreenOrientation 一起看~
<!-- src/components/useMountedDemo.vue -->
<script setup>
import { useMounted } from '@/compositions/useMounted'
const isMounted = useMounted()
</script>
<template>
<h2>useMountedDemo</h2>
<div>{{ isMounted ? 'mounted' : 'unmounted' }}</div>
</template>
先看用法,就是以下這段的縮寫:
const isMounted = ref(false)
onMounted(() => {
isMounted.value = true
})
不過我猜應該是會在其他的 composition 中比較常用到,像是稍後要講的 useSupported。
// src/compositions/useMounted.js
import { getCurrentInstance, onMounted, ref } from 'vue'
export function useMounted() {
const isMounted = ref(false)
const instance = getCurrentInstance()
if (instance) {
onMounted(() => {
isMounted.value = true
}, instance)
}
return isMounted
}
vueuse 原始碼有針對 Vue2 做判斷,上面我實作的版本把這個判斷移除了,先不理 Vue2(?)
這邊有用到 vue3 提供的 API getCurrentInstance,注意這個 API 不推薦在業務功能上使用,因為他是非公開的 API,通常是給一些 package 使用,在官方文件中也找不到這個 API。
可以先想像成 getCurrentInstance 可以取得當下使用 useMounted 的組件實例。
再來一個新奇的就是,原來 onMounted 有第二個參數,這個應該也是非公開用法,文件上找不到。
但這段可以去翻翻看 vue 的原始碼。以下可以先去 clone vue 專案,用全域搜尋對照著看~
先找到 onMounted 看看是否可以傳入第二個參數:
// packages/runtime-core/src/apiLifecycle.ts
export const createHook =
<T extends Function = () => any>(lifecycle: LifecycleHooks) =>
(hook: T, target: ComponentInternalInstance | null = currentInstance) =>
// post-create lifecycle registrations are noops during SSR (except for serverPrefetch)
(!isInSSRComponentSetup || lifecycle === LifecycleHooks.SERVER_PREFETCH) &&
injectHook(lifecycle, hook, target)
export const onMounted = createHook(LifecycleHooks.MOUNTED)
可以看到 onMounted 是 createHook 執行後的回傳值,也就是 (hook: T, target: ComponentInternalInstance | null = currentInstance) => ( // ... 略),所以除了我們平常傳入的 callback function 之外,還能傳入 target,上面提到的 vueuse useMounted 就是把 component instance 當作第二個參數傳給 onMounted 的。
看到這段就有點好奇,我們平常沒傳入 target 的時候,onMounted 是怎麼自動找到 componenet instance 的?
vue 原始碼,由內往外找:
setCurrentInstance(instance) → setupStatefulComponent → setupComponent → baseCreateRenderer.mountComponent → createRenderer → baseCreateRenderer → 最後 return 出熟悉的 createApp
// packages/runtime-core/src/renderer.ts
function baseCreateRenderer() {
// ...略
return {
render,
hydrate,
createApp: createAppAPI(render, hydrate)
}
}
以流程大概是,呼叫 createApp → createRenderer → 接著重點在 baseCreateRenderer.mountComponent 這邊設定好 instance,呼叫 setupComponent(instance) ,setupComponent 裡面又呼叫 setupStatefulComponent(instance) ,在 setupStatefulComponent 裡面呼叫 setCurrentInstance(instance) ,這時候 currentInstance 就被設定為一開始在 mountComponent 拿到的 instance。
回到剛剛提到的 onMounted 那段:
// packages/runtime-core/src/apiLifecycle.ts
export const createHook =
<T extends Function = () => any>(lifecycle: LifecycleHooks) =>
(hook: T, target: ComponentInternalInstance | null = currentInstance) =>
// post-create lifecycle registrations are noops during SSR (except for serverPrefetch)
(!isInSSRComponentSetup || lifecycle === LifecycleHooks.SERVER_PREFETCH) &&
injectHook(lifecycle, hook, target)
export const onMounted = createHook(LifecycleHooks.MOUNTED)
所以推測 target 參數的預設值 currentInstance 會拿到設定好的 currentInstance。
以上實作的 GitHub PR:https://github.com/RhinoLee/30days_vue/pull/11/files
// src/compositions/useSupported.js
import { computed } from 'vue'
import { useMounted } from '@/compositions/useMounted'
export function useSupported(callback) {
const isMounted = useMounted()
return computed(() => {
// to trigger the ref
// eslint-disable-next-line no-unused-expressions
isMounted.value
return Boolean(callback())
})
}
滿單純的 API,比較妙的是直接執行 isMounted.value 來觸發 computed 的響應式依賴 XD。用法的話,以明天會提到的 useDeviceOrientation 來說:
const isSupported = useSupported(() => window && 'DeviceOrientationEvent' in window)
useSupported 會回傳 () => window && 'DeviceOrientationEvent' in window 的 Boolean 結果,就會得到瀏覽器是否支援 DeviceOrientationEvent 的結果。
以上實作的 GitHub PR:https://github.com/RhinoLee/30days_vue/pull/12/files
今天花了一點篇幅在看 Vue 的原始碼,滿慶幸一開始主題不是 Vue 原始碼 30 天,感覺完全是另外一個世界 XD,明天就從 useDeviceOrientation & useScreenOrientation 繼續看下去~